//*************************************************************************************
/** \file  ministroni056.cpp
 *  This file contains a program to run the "Ministrone" tensile tester interface. The 
 *  interface contains an ATMega8, an instrumentation amplifier, digital inputs, and a 
 *  USB to serial chip. The purpose of the program is to record data from a tensile 
 *  test so that an engineering stress-strain diagram can be plotted. 
 *
 *  Revisions:
 *    \li 06-18-2006 JRR Original program, an A/D test program
 *    \li 07-07-2006 JRR Accelerometer test V 0.1
 *    \li 09-10-2006 JRR Debugged current/min/max display, V 0.2
 *    \li 10-22-2006 JRR Packaged for distribution, V 0.3
 *    \li 01-11-2007 JRR Transformed into Minestroni interface program
 *    \li 03-17-2007 JRR Added millivolt conversions and counter reset
 *    \li 04-08-2007 JRR Added help screen
 *    \li 02-02-2010 JRR Changed to main/lib dir. tree, added ch. 0-7 options
 *    \li 01-10-2011 JRR Modified into a really low budget USB oscilloscope with data
 *                       taken in ISR and sent as a batch to be saved or plotted
 *    \li 01-19-2011 JRR Added rising and falling edge triggering; debugged stuff
 *    \li 02-10-2011 JRR Added pre-trigger buffering using template class scope_queue
 *
 *  License:
 *    This file copyright 2007-2011 by JR Ridgely. It is released under the Lesser GNU
 *    public license, version 2. It is intended for educational use only, but the
 *    user is free to use it for any purpose permissible under the LGPL. The author
 *    has no control over the use of this file and cannot take any responsibility
 *    for any consequences of this use. 
 */
//*************************************************************************************

#include <stdlib.h>							// Standard C/C++ library
#include <avr/io.h>							// Port I/O for SFR's
#include <avr/wdt.h>						// Watchdog timer header

#include "scope_queue.h"					// Queue (ring buffer) template library
#include "avr_adc.h"						// Very simple A/D converter driver
#include "rs232int.h"						// Serial port with receive interrupts


//-------------------------------------------------------------------------------------
/// This is the size of the buffer which holds data points waiting to be sent
#define DATA_BUFFER_SIZE	200

/// This is the part of the buffer which holds data taken before an edge trigger event
#define PRETRIG_BUF_SIZE	80

/** This queue holds data points. It is written by an interrupt service 
 *  routine, and it is read by the main program as data is sent to the host
 *  via the serial port.
 */
#if DATA_BUFFER_SIZE < 255
	scope_queue<uint16_t, uint8_t, DATA_BUFFER_SIZE> data_buffer (PRETRIG_BUF_SIZE);

	/// This is the number of data points which have been recorded so far.
	uint8_t points_saved;
#else
	scope_queue<uint16_t, uint16_t, DATA_BUFFER_SIZE> data_buffer (PRETRIG_BUF_SIZE);

	/// This is the number of data points which have been recorded so far.
	uint16_t points_saved;
#endif

/// This is the number of times to read the A/D converter for each measurement
#define N_OVERSAMPLE		1				// Maximum allowable value = 31

/// This is the separator character, written between data columns
#define SEPARATOR			'\t'


//-------------------------------------------------------------------------------------
/** This array contains numbers of milliseconds per point for each timebase setting.
 *  Based on 4 interrupts per millisecond, 100 points per screen, and 5 divisions per 
 *  screen:
 *  \li Mode  time/div   ms/pt  ISR's/pt
 *  \li  0    1 sec      50        200
 *  \li  1    500 ms     25        100
 *  \li  2    200 ms     10        40
 *  \li  3    100 ms     5         20
 *  \li  4    50 ms      2.5       10
 *  \li  5    20 ms      1         4
 *  \li  6    10 ms      0.5       2
 *  \li  7    5 ms       0.25      1
 */

uint8_t inters_per_reading[8] = {200, 100, 40, 20, 10, 4, 2, 1};

/// This counter is used to cause data to be taken at the correct time interval
uint8_t inter_counter;

/// This is the maximum value to which inter_counter counts
uint8_t max_inter_count;

/// This number is the channel on which we're going to take data
uint8_t data_channel;

/// This flag is set true when data capture has been manually triggered
bool manual_trigger = false;

/// This flag is set to tell the ISR that data has been successfully sent
bool sending_done = false;

/// This 32-bit number holds the data checksum as it's being added up
uint32_t data_checksum = 0;


//-------------------------------------------------------------------------------------
/** This enumeration lists the possible trigger modes. Modes defined are as follows:
 *  \li 0 - Manual triggering; AVR takes data when asked to by serial character
 *  \li 1 - Rising edge triggering; data taken when voltage crosses up over threshold
 *  \li 2 - Falling edge triggering; data taken when voltage crosses downward
 */

enum trigger_t {TRIG_MANUAL, TRIG_POS_EDGE, TRIG_NEG_EDGE};

/// This integer holds the triggering mode.
trigger_t trig_mode = TRIG_MANUAL;

/// This is the trigger level for rising or falling edge triggering
uint16_t trig_level = 600;

/// This pointer points to the A/D converter used in the ISR
avr_adc* p_adc = NULL;

/// This pointer points to the UART -- use with caution for debugging only
base_text_serial* p_uart;


//-------------------------------------------------------------------------------------
/** This function reads a trigger level from the serial port. The trigger level must be
 *  a 4 digit decimal integer from 0000 to 1023. If a valid number is read, this 
 *  function sets the trigger level used by the ISR to begin recording data; if the
 *  next 4 digits don't contain a valid trigger level or there's a timeout, nothing is
 *  changed or set.
 *  @param uart A reference to the serial device with which we're communicating
 *  @param new_level A reference to the value for the trigger level, in the range 
 *                   [0..1023], which will be set only if valid data has been read
 */

#define TRIG_LEV_TIMEOUT	100000L

void get_trigger_level (base_text_serial& uart, uint16_t& new_level)
{
	uint16_t num_in = 0;					// Number being read in from serial device
	uint32_t tout_ctr = 0;					// Timeout counter for reading bytes
	uint8_t digit_num = 0;					// Count the digits being read in
	uint8_t char_in;						// Character read from serial port

	// Wait for characters and deal with them as they come in
	while (tout_ctr < TRIG_LEV_TIMEOUT)
	{
		if (uart.check_for_char ())			// If a character came in, read it
		{
			char_in = uart.getchar ();
			if (char_in > '9' || char_in < '0')
			{
				return;
			}

			num_in *= 10;					// Save the digit just received as the
			num_in += char_in - '0';		// least significant digit of the number

			// If we've received all 4 digits and the number is valid, modify the
			// value of the trigger level (it was passed as a reference)
			if ((++digit_num >= 4) && (num_in < 1024))
			{
				new_level = num_in;
				uart << "L " << new_level << endl;
				return;
			}
		}
		tout_ctr++;
	}

	// If we get here, not all 4 characters were read.  Just give up
	uart << "L " << new_level << endl;
}


//-------------------------------------------------------------------------------------
/** This function prints a simple help message.
 *  @param uart The serial device to which the message is printed
 */

void print_help_message (base_text_serial& uart)
{
	uart << PMS ("?: Ministrone v0.5X help") << endl;
	uart << PMS (" 0-7: Set data channel, 0 through 7") << endl;
	uart << PMS (" t:   Set timebase, 0 through 8") << endl;
	uart << PMS (" g:   Go (begin taking and sending data)") << endl;
	uart << PMS (" s:   Stop taking data") << endl;
	uart << PMS (" +:   Set rising edge trigger") << endl;
	uart << PMS (" -:   Set falling edge trigger") << endl;
	uart << PMS (" n:   Set no active trigger") << endl;
	uart << PMS (" l#:  Set trigger level to # (4 digits)") << endl;
	uart << PMS (" v:   send program version ID") << endl;
}


//=====================================================================================
/** This main function runs the tensile tester. After some very simple setup, it enters 
 *  a simple endless loop in which it checks for single-key user commands, then 
 *  responds to those commands appropriately. If an unrecognized command is given, a
 *  helpful message is returned. 
 */

int main (void)
{
	uint8_t char_in = 0;					// Character read from serial port

	#if DATA_BUFFER_SIZE < 255				// The number of points which have been
		uint8_t points_sent = 0;			// transmitted through the UART so far.
	#else									// When they've all been sent, a footer
		uint16_t points_sent = 0;			// will be sent with a checksum
	#endif

	MCUSR = 0;								// Disable watchdog timer during setup
	wdt_disable ();

	data_channel = 0;						// Read channel 0 by default
	points_saved = 0;						// We haven't saved any data yet
// 	take_data_now = false;					// Don't measure until asked to
	inter_counter = 0;						// Counter for data taking timing

	// Set the default value for time per measurement
	max_inter_count = inters_per_reading[0];

	rs232 uart (57600, 0);					// Set up the serial port
	p_uart = &uart;							//////////////////////////////////

	// Set up the A/D converter driver and save a pointer to it
    avr_adc my_adc;							// Prepare the A/D converter
    p_adc = &my_adc;						// Set the pointer so ISR can find it

	// Set up Timer 1 to interrupt ? times every millisecond: Set CTC clear on output 
	// compare match mode; set clock select to IO clock / 8.  For 4 MHz that's one
	// clock tick every 2 us. 
	TCCR1A = 0;
	TCCR1B = (1 << WGM12) | (1 << CS11);
	OCR1A = (uint16_t)(125L * F_CPU / 8000000L);
	#ifdef TIMSK1
		TIMSK1 |= (1 << OCIE1A);
	#else
		TIMSK |= (1 << OCIE1A);
	#endif

	// Activate the watchdog timer to reset if the system is AWOL for 2 seconds
	wdt_enable (WDTO_2S);

	// Enable interrupts so that the serial port can receive characters and the timer
	// can go off to enable taking of data
	sei ();

	while (true)
	{
		// Check for a command character in the buffer; if so, read it and act upon it
		if (uart.check_for_char ())
		{
			char_in = uart.getchar ();			// Read the character

			switch (char_in)
			{
				case '0':						// A number from 0 to 7 means to get
				case '1':						// data from the given channel
				case '2':						// immediately and send it out the
				case '3':						// serial port
				case '4':
				case '5':
				case '6':
				case '7':
					uart << my_adc.read_oversampled ((char_in - '0'), N_OVERSAMPLE) 
						 << endl;
					break;

				case 'c':						// 'c' sets the data channel
					while (!(uart.check_for_char ()));
					char_in = uart.getchar () - '0';
					if (char_in < 8)			// Must be a valid timebase number
					{
						data_channel = char_in;
					}
					break;

				case 'g':						// 'g' means begin taking data
					manual_trigger = true;
					uart << "#" << DATA_BUFFER_SIZE << endl;	// Print a header
					data_checksum = 0;							// Reset checksum
// 					send_a_header = true;
					break;

				case 'v':						// Initialize and/or information(?)
					uart << PMS ("Ministrone V0.56") << endl;
					break;

				case 'l':						// Change level for edge triggering
					get_trigger_level (uart, trig_level);
					break;

				case 's':						// Stop measurements
/*					if (take_data_now == true)
					{
						take_data_now = false;
					}*/
					break;

				case '+':						// Turn on rising edge triggering
					trig_mode = TRIG_POS_EDGE;
					break;

				case '-':						// Turn on falling edge triggering
					trig_mode = TRIG_NEG_EDGE;
					break;

				case 'n':						// Turn on off internal triggering
					trig_mode = TRIG_MANUAL;
					break;

				case 't':						// 't' sets the time per data point
					while (!(uart.check_for_char ()));
					char_in = uart.getchar () - '0';
					if (char_in < 8)			// Must be a valid timebase number
					{
						max_inter_count = inters_per_reading[char_in];
					}
					break;

				case '?':						// '?' is a plea for help
					print_help_message (uart);
					break;

				default:						// If character is not recognized,
					uart.putchar (char_in);		// give the luser some help
					uart << PMS (":WTF?") << endl;
					break;
				};
		}

		// If this is the first data in a bufferfull, send the header first
// 		if (send_a_header)
// 		{
// 			uart << "#" << DATA_BUFFER_SIZE << endl;
// 			send_a_header = false;
// 			points_sent = 0;
// 		}

		// If there's any data in the data queue, print it to the serial port 
		if (!(data_buffer.is_empty ()))
		{
			cli ();
			uint16_t temp_data = data_buffer.get ();
			sei ();
			data_checksum += temp_data;
			uart << temp_data << endl;
			points_sent++;
		}

		// If a full set of data has just been sent, send the checksum
		if (points_sent >= DATA_BUFFER_SIZE)
		{
			uart << "$" << (uint8_t)(data_checksum & 0xFF) << endl;
			points_sent = 0;
			sending_done = true;
		}

		// Reset the watchdog timer so it doesn't cause the processor to reset
		wdt_reset ();
	}										// End main while loop

	return (0);
}


//-------------------------------------------------------------------------------------
/** This function is called from within the ISR to begin measuring and sending data.
 */

// void begin_sending (void)
// {
// 	points_saved = 0;		// Make sure we start with an empty
// 	data_buffer.flush ();	// data buffer
// 	send_a_header = true;
// }


//-------------------------------------------------------------------------------------
// This interrupt service routine takes and saves data if data recording has been 
// activated. It uses the Timer 1 compare match vector; Timer 1 will have been set up 
// to control the rate at which the interrupt occurs. 
//
// The ISR implements a state machine to control when it takes data.  The states are:
//   ISR_IDLE      - Manual triggering mode, no trigger has been requested
//   ISR_MANU_DATA - Manual mode, taking data; the background is sending buffered data
//   ISR_WAIT_SEND - Data has been taken; wait for it to be sent before taking more
//   ISR_WAIT_POS  - Positive edge triggered, watching for up edge, buffering data
//   ISR_WAIT_NEG  - Negative edge triggered, watching for down edge, buffering data

enum isr_state_t {ISR_IDLE, ISR_WAIT_POS, ISR_WAIT_NEG, ISR_MANU_DATA, ISR_WAIT_SEND};

ISR (TIMER1_COMPA_vect)
{
	static isr_state_t isr_state = ISR_IDLE;	// ISR needs to run a state machine
	static uint16_t prev = 0;					// Save previous reading for triggering
	uint16_t curr;								// Store current A/D reading here

	// First, count out the right number of interrupts so that data is taken at the
	// correct time interval
	if (++inter_counter >= max_inter_count)
	{
		inter_counter = 0;

		// Run the state machine which controls what we're doing at the moment
		switch (isr_state)
		{
			//=========================================================================
			// We're in the idle state, so we don't need to check or save anything
			case (ISR_IDLE):
				//---------------------------------------------------------------------
				// Transition: If changed to non-manual mode, change the state
				if (trig_mode == TRIG_POS_EDGE)
				{
					points_saved = 0;
					data_buffer.flush ();
					curr = p_adc->read_oversampled (data_channel, N_OVERSAMPLE);
					prev = curr;
					data_buffer.set_pre_trigger_mode ();
					isr_state = ISR_WAIT_POS;
				}
				else if (trig_mode == TRIG_NEG_EDGE)
				{
					points_saved = 0;
					data_buffer.flush ();
					curr = p_adc->read_oversampled (data_channel, N_OVERSAMPLE);
					prev = curr;
					data_buffer.set_pre_trigger_mode ();
					isr_state = ISR_WAIT_NEG;
				}
				//---------------------------------------------------------------------
				// Transition: If in manual mode and the go signal has been read, go
				// right to the save and send data state
				else if (manual_trigger)
				{
					manual_trigger = false;
					points_saved = 0;				// Make sure we start with an
					data_buffer.flush ();			// empty data buffer
					isr_state = ISR_MANU_DATA;
				}
				break;

			//=========================================================================
			// We're in positive edge triggered mode waiting for the up transition.
			// Data is being saved in the buffer (the jam() method forces old data to
			// be overwritten if the buffer is full). 
			case (ISR_WAIT_POS):
				curr = p_adc->read_oversampled (data_channel, N_OVERSAMPLE);
				data_buffer.jam (curr);

				//---------------------------------------------------------------------
				// Transition: If a rising edge is seen, begin sending data and go to
				// the ISR_TAKE_DATA state in which data is being sent
				if ((curr > trig_level) && (prev <= trig_level))
				{
					points_saved = PRETRIG_BUF_SIZE;
					isr_state = ISR_MANU_DATA;
				}

				//---------------------------------------------------------------------
				// Transition:  If in another mode, change to the correct state
				if (trig_mode == TRIG_MANUAL)
				{
					isr_state = ISR_IDLE;
				}
				else if (trig_mode == TRIG_NEG_EDGE)
				{
					isr_state = ISR_WAIT_NEG;
				}

				prev = curr;
				break;

			//=========================================================================
			// We're in negative edge triggered mode waiting for the down transition
			case (ISR_WAIT_NEG):
				//---------------------------------------------------------------------
				// Transition: If a rising edge is seen, begin sending data and go to
				// the ISR_TAKE_DATA state in which data is being sent
				if ((curr < trig_level) && (prev >= trig_level))
				{
					points_saved = PRETRIG_BUF_SIZE;
					isr_state = ISR_MANU_DATA;
				}

				//---------------------------------------------------------------------
				// Transition:  If in another mode, change to the correct state
				if (trig_mode == TRIG_MANUAL)
				{
					isr_state = ISR_IDLE;
				}
				else if (trig_mode == TRIG_POS_EDGE)
				{
					isr_state = ISR_WAIT_POS;
				}

				prev = curr;
				break;

			//=========================================================================
			// We've been triggered; take data. It will be sent from the buffer
			case (ISR_MANU_DATA):
				// Take data and save it
				curr = p_adc->read_oversampled (data_channel, N_OVERSAMPLE);
				prev = curr;
				data_buffer.jam (curr);

				//---------------------------------------------------------------------
				// Transition: If the buffer is full, wait for data to be sent
				if (++points_saved >= DATA_BUFFER_SIZE)
				{
					isr_state = ISR_WAIT_SEND;
				}
				break;

			//=========================================================================
			// Data has been taken; wait for the buffered data to all be sent out
			case (ISR_WAIT_SEND):
				// We don't take data at this time, because we're just waiting

				//---------------------------------------------------------------------
				// Transition: If all the data has been sent, go back to the idle state
				// (if we're in a triggered mode, the idle state will transition 
				// immediately to the waiting state for the correct triggered mode)
				if (sending_done)
				{
					sending_done = false;
					isr_state = ISR_IDLE;
				}
				break;

			//=========================================================================
			// This only happens if we're in an illegal state somehow
			default:
				isr_state = ISR_IDLE;
				break;
		}
	}
}
